﻿
using Boilen.Primitives.Members;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;


namespace Boilen.Primitives {

    public sealed partial class PartialType {

        /// <summary>
        /// Generates the code to the specified writer.
        /// </summary>
        public void Run( ICodeWriter writer ) {
            this.Run( writer, true );
        }

        /// <summary>
        /// Generates the code to the write method.
        /// </summary>
        public void Run( Action<string> write ) {
            Ensure.NotNull( write );

            var writer = new StringBuilderCodeWriter( );
            this.Run( writer, false );

            write( writer.ToString( ) );
        }


        private void Run( ICodeWriter writer, bool includeNewLine ) {
            Ensure.NotNull( writer );

            try {
                if( includeNewLine )
                    writer.WriteLine( );

                this.RunCore( writer );
            }
            catch( Exception e ) {
                writer.WriteLine( "ERROR" );
                writer.WriteLine( "// Code generation failed:" );
                foreach( string line in e.ToString( ).Split( new[] { Environment.NewLine }, StringSplitOptions.None ) )
                    writer.WriteLine( "//   " + line );

                throw;
            }
        }

        private void RunCore( ICodeWriter writer ) {
            // Prepare implementers for writing.
            for( int i = 0; i < this.Implementers.Count; ++i ) {
                var implementer = this.Implementers[i];
                implementer.Prepare( );
            }

            // Get the members and check if any use validation.
            var members = this.GetMembers( );
            Extensions usedExtensions = Util.GetUsedExtensions( members );
            if( this.IsFreezable )
                usedExtensions |= Extensions.Freezable;

            if( usedExtensions.HasFlag( Extensions.Validation ) )
                this.AddSubNamespace( GlobalSettings.ValidationSubNamespace );
            if( usedExtensions.HasFlag( Extensions.DependencyProperties ) )
                this.AddSubNamespace( GlobalSettings.DependencyPropertySubNamespace );
            if( usedExtensions.HasFlag( Extensions.Freezable ) )
                this.AddSubNamespace( GlobalSettings.FreezableSubNamespace );

            var ptm = new PartialTypeMembers( this, members );


            // Write "using _;" declarations.
            this.WriteUsingDeclarations( writer );

            // Write "namespace _" declaration.
            writer.WriteLine( "namespace {0}", this.Namespace );
            using( Enclose.Braces( writer ) ) {
                writer.WriteLine( );

                // Write "using BaseType = SilverlightBaseType;", if necessary.
                if( !string.IsNullOrEmpty( this.SilverlightBaseTypeName ) ) {
                    writer.WriteLineUnindented( "#if {0}", CompilationSymbol.Silverlight.Symbol );
                    writer.WriteLine( "using {0} = {1};", this.BaseTypeName, this.SilverlightBaseTypeName );
                    writer.WriteLineUnindented( "#endif" );
                    writer.WriteLine( );
                }

                // Write type attributes.
                WriteAttributes( writer, members );

                // Write partial type declaration and any implemented interfaces.
                WriteInsertion( writer, InsertionPoint.BeforeTypeDeclaration );
                writer.Write( "partial {0} {1}", this.Kind, this.TypeName );
                this.WriteInterfaceDeclarations( writer, members );
                WriteInsertion( writer, InsertionPoint.AfterTypeDeclaration );
                writer.WriteLine( );

                // Write members of each implementer.
                using( Enclose.Braces( writer ) ) {
                    WriteInsertion( writer, InsertionPoint.Body );
                    ptm.Write( writer );
                }

                writer.WriteLine( );
            }
        }

        private void WriteInsertion( ICodeWriter writer, InsertionPoint location ) {
            string insertion;
            if( this.insertedText_.TryGetValue( location, out insertion ) )
                writer.Write( insertion );
        }

        private void WriteUsingDeclarations( ICodeWriter writer ) {
            var usedNamespaces = this.TypeRepository.UsedNamespaces
                .Where( n => !this.Namespace.StartsWith( n ) )
                .OrderBy( n => n.Contains( '=' ) ? "Z" + n : n )
                // Promote "System" using declarations above others.
                .GroupBy( n => n.StartsWith( "System" ) )
                .OrderBy( g => g.Key ? 0 : 1 )
                .SelectMany( g => g );

            foreach( string nmspace in usedNamespaces )
                writer.WriteLine( "using {0};", nmspace );
            WriteInsertion( writer, InsertionPoint.Usings );

            writer.WriteLine( );
            writer.WriteLine( );
        }

        private static void WriteAttributes( ICodeWriter writer, IEnumerable<Member> members ) {
            var attributeMembers = members.OfType<AttributeMember>( ).ToArray( );
            if( attributeMembers.Any( ) ) {
                // Write type attributes.
                Member.WriteMembers( writer, attributeMembers );
            }
        }

        private void WriteInterfaceDeclarations( ICodeWriter writer, IEnumerable<Member> members ) {
            var inheritanceMembers = members.OfType<InheritanceMember>( ).ToArray( );
            if( this.IsDerivedType || inheritanceMembers.Any( ) ) {
                // Write base type, if any.
                bool hasUnconditionalInheritors = inheritanceMembers.Any( i => i.Condition.IsUnconditional );
                bool preceded = this.IsDerivedType || hasUnconditionalInheritors;
                if( preceded ) {
                    writer.Write( " :" );
                    if( this.IsDerivedType )
                        writer.Write( " " + this.BaseTypeName + (hasUnconditionalInheritors ? "," : "") );
                }

                // Write interface members, if any.
                Member.WriteConditionalMembers(
                    writer,
                    inheritanceMembers,
                    ( i, last ) => {
                        if( i >= 0 ) {
                            writer.Write( "," );
                            writer.WriteLine( );
                        }
                    },
                    ( groups, i ) => {
                        if( i > 0 || (i == 0 && !hasUnconditionalInheritors) )
                            writer.WriteLine( "    {0}", i > 0 || preceded ? "," : ":" );
                        else if( i == 0 )
                            writer.WriteLine( );
                    }
                );
            }
        }


        private sealed class PartialTypeMembers {
            private enum MemberId {
                Unsupported = -1,
                Field = 0,
                Initializer,
                Accessor,
                Method,
                Group,
                Count
            }

            private readonly PartialType Parent;
            private readonly List<Member>[] Lists;


            public PartialTypeMembers( PartialType parent, IEnumerable<Member> members ) {
                // Create lists for each top-level member type.
                this.Parent = parent;
                this.Lists = Enumerable.Range( 0, (int)MemberId.Count ).Select( _ => new List<Member>( ) ).ToArray( );
                this.PopulateLists( members );
            }


            public void Write( ICodeWriter writer ) {
                bool listWritten = false;

                writer.WriteLine( );
                foreach( var list in this.Lists ) {
                    if( list.Any( ) ) {
                        if( listWritten ) {
                            writer.WriteLine( );
                            writer.WriteLine( );
                        }
                        else {
                            listWritten = true;
                        }

                        Member.WriteMembers( writer, list );
                    }
                }
                writer.WriteLine( );
            }


            private void PopulateLists( IEnumerable<Member> members ) {
                // Separate members into top-level types.
                var initType = new List<InitializationMember>( );
                var initFromParam = new List<InitializationMember>( );
                var initFromValue = new List<InitializationMember>( );
                var initTemplate = new List<InitializationMember>( );
                foreach( Member member in members ) {
                    MemberId id = GetMemberId( member );
                    if( id == MemberId.Unsupported )
                        continue;

                    // Initializer members will be placed in constructors or initializer methods.
                    if( id == MemberId.Initializer ) {
                        var initializer = (InitializationMember)member;
                        var initCollection = initializer.IsInstance
                            ? (initializer.IsParameterValue ? initFromParam : initFromValue)
                            : (initializer.IsOnApplyTemplate ? initTemplate : initType);
                        initCollection.Add( initializer );
                    }
                    // All other top-level members will be written directly.
                    else {
                        this.Lists[(int)id].Add( member );
                    }
                }


                // Create constructors and methods for initializers.
                var initializers = this.Lists[(int)MemberId.Initializer];
                var helpers = new List<Member>( );

                // Static type constructor.
                if( initType.Any( ) ) {
                    var staticConstructorBody = new BlockMember( ".cctor", w => {
                        Member.WriteMembers( w, initType );

                        w.WriteLine( );
                        w.WriteLine( this.Parent.TypeNamePrefix + ".InitializeType();" );
                    } );
                    var staticConstructor = new MethodMember( this.Parent.SimpleTypeName, "", staticConstructorBody ) { Modifiers = "static" };
                    staticConstructor.AddAttributes( this.Parent.cctorAttributes_ );

                    if( this.Parent.Type.IsClass )
                        staticConstructor.Attributes.Add( AttributeMember.SuppressMessage(
                            "Microsoft.Performance", "CA1810:InitializeReferenceTypeStaticFieldsInline", "AUTOGEN: Initializes static members."
                        ) );

                    initializers.Add( staticConstructor );
                    helpers.Add( new MethodMember( "InitializeType", "void", null ) { Modifiers = "static" } );
                }

                // Instance constructors.
                if( initFromParam.Any( ) || initFromValue.Any( ) ) {
                    string constructorModifiers = Util.Lowercase( this.Parent.ConstructorAccessibility.ToString( ) );

                    ParameterMember[] baseInitFromParam = new ParameterMember[0];
                    var baseConstructorParameters = this.Parent.BaseType.GetConstructors( BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance ).OrderByDescending( c => c.GetParameters( ).Length );
                    if( baseConstructorParameters.Any( ) )
                        baseInitFromParam = MethodMember.GetParameters( baseConstructorParameters.First( ), this.CreateParameter ).ToArray( );

                    var initOnlyFromValue = FilterInitializers( initFromValue, initFromParam );
                    var initOnlyFromParam = FilterInitializers( initFromParam, initFromValue );
                    bool writeFullConstructor = initFromParam.Any( ) || baseInitFromParam.Any( ) || (initFromValue.Any( ) && this.Parent.Type.IsClass);
                    bool writeMinimalConstructor = initOnlyFromParam.Length != initFromParam.Count && (this.Parent.Type.IsClass || initOnlyFromParam.Any( ));

                    // Fully-parameterized instance constructor.
                    Doc docDependency = null;
                    if( writeFullConstructor ) {
                        string baseArguments = Util.Join( baseInitFromParam, ", ", ( i, baseParam ) => baseParam.Name );
                        string name = baseInitFromParam.Any( )
                            ? "    : base(" + baseArguments + ")"
                            : ".ctor";

                        var fullConstructorBody = new BlockMember( name, w => {
                            if( initFromParam.Any( ) ) {
                                Member.WriteMembers( w, initFromParam );

                                if( initOnlyFromValue.Any( ) )
                                    w.WriteLine( );
                            }

                            if( initOnlyFromValue.Any( ) )
                                Member.WriteMembers( w, initOnlyFromValue );

                            w.WriteLine( );
                            w.WriteLine( "this.InitializeInstance();" );
                        } ) { WriteName = baseInitFromParam.Any( ) };
                        var fullConstructor = new MethodMember( this.Parent.SimpleTypeName, "", fullConstructorBody ) { Modifiers = constructorModifiers };
                        var orderedParameters = initOnlyFromParam.Concat( initFromParam.Except( initOnlyFromParam ) );
                        this.AddParameters( fullConstructor, true, baseInitFromParam, orderedParameters );
                        fullConstructor.AddAttributes( this.Parent.ctorAttributes_ );
                        docDependency = fullConstructor.Doc;

                        initializers.Add( fullConstructor );
                        helpers.Add( new MethodMember( "InitializeInstance", "void", null ) );
                    }

                    // Minimally-parameterized default value constructor.
                    if( writeMinimalConstructor ) {
                        var minimalInitFromValue = FilterInitializers( initFromValue, initOnlyFromValue );
                        string baseArguments = Util.Join( baseInitFromParam, ", ", ( i, baseParam ) => baseParam.Name );
                        string minArguments = Util.Join( initOnlyFromParam.Concat( minimalInitFromValue ), ", ", ( i, init ) => init.Value );
                        string arguments = baseArguments + (baseArguments.Length > 0 && minArguments.Length > 0 ? ", " : "") + minArguments;
                        string name = "    : this(" + arguments + ")";

                        var minimalConstructorBody = new BlockMember( name, "" ) { WriteName = true };
                        var minimalConstructor = new MethodMember( this.Parent.SimpleTypeName, "", minimalConstructorBody ) { Modifiers = constructorModifiers };
                        this.AddParameters( minimalConstructor, false, baseInitFromParam, initOnlyFromParam );
                        minimalConstructor.DocDependencies.Add( docDependency );
                        initializers.Add( minimalConstructor );
                    }
                }

                // OnApplyTemplate initializer method.
                if( initTemplate.Any( ) ) {
                    const string OnApplyTemplate = "OnApplyTemplate";
                    const string InitializeTemplateParts = "InitializeTemplateParts";

                    var onApplyTemplateBody = new BlockMember( OnApplyTemplate, w => {
                        w.WriteLine( "base.{0}();", OnApplyTemplate );
                        w.WriteLine( );

                        Member.WriteMembers( w, initTemplate, ( i, last ) => w.WriteLine( ) );

                        w.WriteLine( );
                        w.WriteLine( "this.{0}();", InitializeTemplateParts );
                    } );
                    var onApplyTemplate = new MethodMember( OnApplyTemplate, "void", onApplyTemplateBody ) {
                        Doc = new Doc( this.Parent.DocTypeName, this.Parent.TypeName, this.Parent.DocTypeName, null ) {
                            InheritFrom = "System.Windows.FrameworkElement.OnApplyTemplate"
                        },
                        Modifiers = "public override"
                    };

                    initializers.Add( onApplyTemplate );
                    var onApplyTemplateHelper = new MethodMember( InitializeTemplateParts, "void", null );
                    helpers.Add( onApplyTemplateHelper );
                }

                initializers.AddRange( helpers );
            }


            private ParameterMember CreateParameter( string name, Type type, string description ) {
                string typeName = this.Parent.TypeRepository.GetTypeName( type );
                return new ParameterMember( name, typeName );
            }

            private static InitializationMember[] FilterInitializers( IEnumerable<InitializationMember> source, IEnumerable<InitializationMember> exclude ) {
                return source
                    .Where( sourceItem => !exclude.Any( excludedItem => sourceItem.Name == excludedItem.Name ) )
                    .ToArray( );
            }

            private void AddParameters( MethodMember constructor, bool isFullConstructor, IEnumerable<ParameterMember> baseParameters, IEnumerable<InitializationMember> parameters ) {
                const string Summary = "Initializes a new instance of the %see:type% {0}{1}.";
                string parameterNames = Util.Join(
                    baseParameters.Select( p => p.Name ).Concat( parameters.Select( p => p.Value ) ),
                    ( i, last ) => last ? i == 1 ? " and " : ", and " : ", ",
                    ( i, p ) => (i == 0 ? " with the specified " : "") + p
                );

                string name = this.Parent.TypeName;
                string type = this.Parent.DocTypeName;
                string fullyQualifiedName = this.Parent.SimpleTypeName + "(" + Util.Join( baseParameters.Select( p => Doc.GetDocTypeName( p.Type, null ) ).Concat( parameters.Select( p => Doc.GetDocTypeName( p.Type, null ) ) ), "," ) + ")";
                constructor.Doc = new Doc( fullyQualifiedName, name, type, null )
                    .AddSummary( Summary, this.Parent.Type.IsClass ? "class" : "struct", parameterNames );

                if( baseParameters.Any( ) ) {
                    if( GlobalSettings.ExternalDocumentationPrefix != null && !this.Parent.IsInternal )
                        constructor.Doc.UseIncludeTag( );
                    else
                        constructor.Doc.InheritFrom = "";

                    foreach( var baseParameter in baseParameters )
                        constructor.Parameters.Add( baseParameter );
                }

                foreach( var parameter in parameters ) {
                    constructor.Parameters.Add( new ParameterMember( parameter.Value, parameter.Type ) { Doc = parameter.Doc } );

                    constructor.Doc.AddExceptions( parameter.Guards );
                    if( isFullConstructor )
                        constructor.Body.AddGuards( parameter.Guards );
                }
            }

            private static MemberId GetMemberId( Member member ) {
                if( member is FieldMember )
                    return MemberId.Field;
                if( member is InitializationMember )
                    return MemberId.Initializer;
                if( member is AccessorMember )
                    return MemberId.Accessor;
                if( member is MethodMember )
                    return MemberId.Method;
                if( member is MemberGroup )
                    return MemberId.Group;
                if( member is AttributeMember || member is InheritanceMember )
                    return MemberId.Unsupported;

                throw new InvalidOperationException( "Unexpected member: " + member );
            }
        }

    }

}
